/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.nanosn.coffeemaker.java;

import com.nanosn.coffeemaker.Project;
import com.nanosn.coffeemaker.exceptions.ApplicationNotFoundException;
import com.nanosn.coffeemaker.exceptions.InvalidModelException;
import com.nanosn.coffeemaker.exceptions.NoModelsFoundException;
import com.nanosn.coffeemaker.project.ProjectSettings;
import com.nanosn.coffeemaker.project.ProjectSettingsFactory;
import com.nanosn.util.FileUtility;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.codehaus.groovy.control.CompilationFailedException;

/**
 * TODO: singlton?
 * @author selkhateeb
 */
public class ProjectClassLoader extends URLClassLoader {

    private static ProjectClassLoader instance;

    /**
     * TODO: refine exceptions
     * @return
     * @throws Exception
     */
    public static ProjectClassLoader getInstance() throws Exception {
        if (instance != null) {
            return instance;
        }

        List<URL> urls = new ArrayList<URL>();
        /*
         * Now this assumes that all installed apps are in the same
         * 'workspace' .. which is a directory above the package
         */
        File projectDirectory = new File(Project.getDevelopmentDirectory());
        urls.add(projectDirectory.toURI().toURL());

        instance = new ProjectClassLoader(
                ProjectClassLoader.class.getClassLoader(),
                urls.toArray(new URL[urls.size()]));
        return instance;

    }

    /**
     * Initializes a new instance of the class loader
     * @param parent we need to keep track of the main class loader
     * TODO: check if we can use this class's class loader
     * @param urls the urls to the classes in the INSTALLED_APPS
     */
    public ProjectClassLoader(ClassLoader parent, URL... urls) {
        super(urls, parent);
    }

    /**
     * Complies the java source file
     * @param javaFile
     * @return true on success
     */
    private boolean compile(String javaFile) {
        //TODO: Pipe output to a writer so that we can have fun with colors
        //TODO: throw compiling error
        int compile = com.sun.tools.javac.Main.compile(new String[]{javaFile});
        return compile == 0;
    }

    @Override
    public Class loadClass(String name, boolean resolve) throws
            ClassNotFoundException {


        //Compile if needed
        JavaClass javaClass;

        try {
            javaClass = new JavaClass(name, getURLs());

            //We got 2 conditions .. is it uptodate
            if (!javaClass.isUpToDate()) {
                compile(javaClass.getJavaFile().getAbsolutePath());
            }
        }
        catch (URISyntaxException ex) {
            throw new ClassNotFoundException(ex.getMessage());
        }
        catch (ClassNotFoundException ex) {
            //well .. this class does not have source code that we are intrested in
            // so just return the Class object
        }
        return super.loadClass(name, resolve);
    }

    public void ValidateModelClasses() throws
            ApplicationNotFoundException,
            NoModelsFoundException,
            InvalidModelException {
        try {
            //Get all INSTALLED_APPS
            ProjectSettings settings = ProjectSettingsFactory.getInstance();
            for (String app : settings.getInstalledApps()) {
                ValidateModelClassesFor(app);
            }

        }
        catch (CompilationFailedException ex) {
            Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
        catch (IOException ex) {
            Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
        catch (InstantiationException ex) {
            Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
        catch (IllegalAccessException ex) {
            Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     *
     * @param applicationPackage the full package name without ".models"
     * ie "prjectname.appname"
     */
    public void ValidateModelClassesFor(String applicationPackage)
            throws ApplicationNotFoundException,
                   NoModelsFoundException,
                   InvalidModelException {
        File applicationDirectory = locate(applicationPackage);
        if (applicationDirectory == null) {
            throw new ApplicationNotFoundException(applicationPackage);
        }

        File modelsDirectory = new File(applicationDirectory, "models");

        if (!modelsDirectory.exists()) {
            throw new NoModelsFoundException(applicationPackage);
        }

        //get all model files
        List<File> modelFiles = FileUtility.getFileListing(modelsDirectory);

        //For each file .. load the class to make sure its valid
        for (File file : modelFiles) {

            //Ignore source files
            if (file.getAbsolutePath().endsWith(".java")) {
                continue;
            }

            String className = getClassNameFor(file, applicationPackage);

            try {
                loadClass(className);
            }
            catch (ClassNotFoundException ex) {
                throw new InvalidModelException(className);
            }
        }
    }

    private String getClassNameFor(File file, String applicationPackage) {

        String applicationRelativeDirectory = applicationPackage.replace(".", "/");
        String classFile =
                file.getAbsolutePath().substring(file.getAbsolutePath().indexOf(applicationRelativeDirectory));

        String className = classFile.replace(".class", "").replace("/", ".");
        return className;
    }

    public Class<?>[] getModelClasses() throws InvalidModelException {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        try {
            //Get all INSTALLED_APPS
            ProjectSettings settings = ProjectSettingsFactory.getInstance();
            for (String app : settings.getInstalledApps()) {
                classes.addAll(Arrays.asList(getModelClassesFor(app)));
            }

        }
        catch (CompilationFailedException ex) {
            Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
        catch (IOException ex) {
            Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
        catch (InstantiationException ex) {
            Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
        catch (IllegalAccessException ex) {
            Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
        return classes.toArray(new Class[classes.size()]);
    }

    public Class<?>[] getModelClassesFor(String applicationPackage) throws InvalidModelException {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        File applicationDirectory = locate(applicationPackage);
        File modelsDirectory = new File(applicationDirectory, "models");

        List<File> modelFiles = FileUtility.getFileListing(modelsDirectory);
        for (File file : modelFiles) {
            try {

                //Ignore source files
                if (file.getAbsolutePath().endsWith(".java")) {
                    continue;
                }
                classes.add(loadClass(getClassNameFor(file, applicationPackage)));
            }
            catch (ClassNotFoundException ex) {
                throw new InvalidModelException(applicationPackage);
            }
        }

        return classes.toArray(new Class[classes.size()]);
    }

    private File locate(String applicationPackage) {
        String applicationPath = applicationPackage.replace(".", "/");
        for (URL url : this.getURLs()) {
            try {
                File dir = new File(new File(url.toURI()), applicationPath);

                Logger.getLogger(ProjectClassLoader.class.getName()).log(
                        Level.FINEST, "locate(" + applicationPackage + ")" + "..." + dir.getAbsolutePath());
                if (dir.exists()) {
                    return dir;
                }
            }
            catch (URISyntaxException ex) {
                Logger.getLogger(ProjectClassLoader.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return null;
    }
}
